Skip to content

feat(core): add startup manifest for faster cold starts#5842

Draft
killagu wants to merge 5 commits intonextfrom
feat/startup-manifest
Draft

feat(core): add startup manifest for faster cold starts#5842
killagu wants to merge 5 commits intonextfrom
feat/startup-manifest

Conversation

@killagu
Copy link
Copy Markdown
Contributor

@killagu killagu commented Mar 27, 2026

Summary

WIP: 通过静态化启动时的文件 I/O metadata 来优化 Egg/tegg 框架冷启动性能。

  • 新增 ManifestStore 类,将 resolveModule 路径、globby 文件发现结果、tegg 模块描述符缓存到 .egg/manifest.json
  • 启动时通过 stat-based fingerprint(mtime+size)校验 manifest 有效性,跳过昂贵的文件扫描
  • tegg 层:ModuleLoader 接受预计算的 decorated files 列表,跳过 70-80% 的 import() 调用
  • tegg config 插件:有 manifest 时跳过深度 globby.sync('**/package.json') 扫描
  • egg-bin:新增 manifest generate|validate|clean CLI 命令
  • 首次启动自动生成 manifest(ready hook 中的 dumpManifest

主要优化点

消除的 I/O 预期加速
tegg 模块扫描 每模块 globby + 70-80% import() 40-60%
egg core resolveModule 数百次 fs.existsSync 30-50%
egg core FileLoader 4-7 次 globby.sync 10-20%

TODO

  • 性能基准测试(有/无 manifest 对比)
  • 更完整的 egg-bin manifest generate(通过完整 app boot 收集数据)
  • 单元测试覆盖 ManifestStore

Test plan

  • @eggjs/core 351 tests passed
  • egg 352 tests passed
  • tegg plugin/loader/metadata 72 tests passed
  • 所有修改包 typecheck 通过

🤖 Generated with Claude Code

Add a ManifestStore system that caches file I/O metadata (resolveModule
paths, globby file discovery, tegg module descriptors) to .egg/manifest.json.
On subsequent startups, the manifest is loaded and validated (via stat-based
fingerprinting of lockfile and config directory), skipping expensive
filesystem scanning operations.

Key changes:
- ManifestStore class with load/validate/generate/write/clean APIs
- EggLoader: manifest-aware resolveModule() and FileLoader.parse()
- tegg: ModuleLoader accepts precomputed decorated files from manifest
- tegg config plugin: skips deep globby scan when manifest available
- egg-bin: new `manifest generate|validate|clean` CLI command
- Auto-generates manifest on first startup (dumpManifest in ready hook)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 382d1268-8500-4ff9-99a4-e07520232387

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/startup-manifest

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Mar 27, 2026

Deploying egg with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3ddfe9d
Status: ✅  Deploy successful!
Preview URL: https://abb136ac.egg-cci.pages.dev
Branch Preview URL: https://feat-startup-manifest.egg-cci.pages.dev

View logs

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Mar 27, 2026

Deploying egg-v3 with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3ddfe9d
Status: ✅  Deploy successful!
Preview URL: https://ad5f5ad4.egg-v3.pages.dev
Branch Preview URL: https://feat-startup-manifest.egg-v3.pages.dev

View logs

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 27, 2026

Codecov Report

❌ Patch coverage is 85.54217% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.63%. Comparing base (1980cf1) to head (3ddfe9d).
⚠️ Report is 2 commits behind head on next.

Files with missing lines Patch % Lines
packages/core/src/lifecycle.ts 20.00% 8 Missing ⚠️
tegg/plugin/config/src/app.ts 60.00% 5 Missing and 1 partial ⚠️
tegg/plugin/tegg/src/app.ts 0.00% 2 Missing and 1 partial ⚠️
packages/core/src/loader/egg_loader.ts 87.50% 2 Missing ⚠️
packages/egg/src/lib/egg.ts 71.42% 2 Missing ⚠️
packages/core/src/loader/manifest.ts 98.64% 1 Missing ⚠️
packages/egg/src/lib/loader/AppWorkerLoader.ts 50.00% 1 Missing ⚠️
tegg/plugin/tegg/src/lib/EggModuleLoader.ts 90.90% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             next    #5842      +/-   ##
==========================================
- Coverage   85.64%   85.63%   -0.01%     
==========================================
  Files         665      666       +1     
  Lines       13004    13144     +140     
  Branches     1495     1515      +20     
==========================================
+ Hits        11137    11256     +119     
- Misses       1743     1763      +20     
- Partials      124      125       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a startup manifest system designed to accelerate application cold starts by caching expensive file discovery and module resolution results. Key changes include the implementation of a ManifestStore for managing manifest lifecycle, integration of caching logic into EggLoader and FileLoader, and the addition of a manifest command in egg-bin for manual management. Review feedback focuses on refining error handling within the manifest loading and fingerprinting utilities to ensure that system errors (like permission issues) are not silently swallowed and treated as missing files.

Comment on lines +73 to +76
} catch {
debug('manifest not found at %s', manifestPath);
return null;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The catch block is too broad and will treat any file read error as 'file not found'. This can hide underlying issues like file permission errors. It's better to specifically handle the ENOENT (file not found) case and log other errors for easier debugging.

    } catch (err: any) {
      if (err.code === 'ENOENT') {
        debug('manifest not found at %s', manifestPath);
      } else {
        debug('failed to read manifest at %s: %o', manifestPath, err);
      }
      return null;
    }

Comment on lines +183 to +188
try {
fs.unlinkSync(manifestPath);
debug('manifest removed: %s', manifestPath);
} catch {
// file doesn't exist, nothing to do
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The catch block is too broad and will swallow any error from fs.unlinkSync, not just when the file doesn't exist. This can hide permission issues or other problems, and the comment '// file doesn't exist, nothing to do' is misleading. It's better to only ignore ENOENT errors and log others for debugging purposes.

    try {
      fs.unlinkSync(manifestPath);
      debug('manifest removed: %s', manifestPath);
    } catch (err: any) {
      if (err.code !== 'ENOENT') {
        debug('Error removing manifest file at %s: %o', manifestPath, err);
      }
    }

Comment on lines +195 to +200
try {
const stat = fs.statSync(filepath);
return `${stat.mtimeMs}:${stat.size}`;
} catch {
return null;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This try-catch block swallows all errors from fs.statSync. While returning null for a non-existent file is reasonable, this will also return null for other errors like permission denied. This might be acceptable as the caller treats null as 'missing', but logging the error for non-ENOENT cases would improve diagnostics.

    try {
      const stat = fs.statSync(filepath);
      return stat.mtimeMs + ':' + stat.size;
    } catch (err: any) {
      if (err.code !== 'ENOENT') {
        debug('Failed to stat file %s: %o', filepath, err);
      }
      return null;
    }

Comment on lines +222 to +226
try {
realPath = fs.realpathSync(dirpath);
} catch {
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This try-catch block silently ignores errors from fs.realpathSync. This can lead to an incomplete fingerprint if there are, for example, permission errors. An incomplete fingerprint might not correctly detect changes, causing a stale manifest to be considered valid. It would be better to log these errors for debugging purposes. A similar issue exists for the fs.readdirSync call on lines 232-236.

Suggested change
try {
realPath = fs.realpathSync(dirpath);
} catch {
return;
}
try {
realPath = fs.realpathSync(dirpath);
} catch (err: any) {
debug('Failed to get realpath for %s: %o', dirpath, err);
return;
}

killagu and others added 4 commits March 27, 2026 22:42
- Add `metadataOnly` option to EggCore/EggLoader for manifest generation
  without triggering external SDK connections
- Add `loadMetadata` lifecycle hook to ILifecycleBoot — called instead of
  configWillLoad/configDidLoad/didLoad/willReady in metadataOnly mode
- Short-circuit AppWorkerLoader.load() after loadCustomApp in metadataOnly
- Skip agent creation in startEgg() when metadataOnly
- Prevent didReady from firing in metadataOnly mode
- tegg config plugin: implement loadMetadata for module scanning
- tegg plugin: implement loadMetadata using shared buildTeggManifestData()
- Extract buildRequireExecArgv() to BaseCommand (shared by dev + manifest)
- egg-bin manifest generate: fork manifest-generate.mjs with metadataOnly
- Remove baseDir validation from manifest (build env ≠ runtime env)
- Add E2E tests for manifest cache usage during app loading

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- AppWorkerLoader: collect router resolveModule in metadataOnly mode
  instead of skipping loadRouter entirely
- Fix agent?.close() for metadataOnly mode (agent not created)
- Split manifest.test.ts into 4 focused test files + shared helper
- Remove inline E2E tests from packages/core/test/
- Add ecosystem-ci/scripts/verify-manifest.mjs for E2E verification
- E2E workflow: generate manifest → boot cnpmcore → verify cache hits

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@eggjs/core is used by the manifest command and scripts/manifest-generate.mjs
but tsdown doesn't detect it since the script is copied, not bundled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous test copied files between directories which loses mtime
precision (sub-ms floating point drift). Instead, test by tampering the
stored baseDir in an existing manifest to prove it's not validated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
killagu added a commit that referenced this pull request Mar 28, 2026
Add ManifestStore class to cache startup filesystem I/O results
(resolveModule lookups, globby scans) in `.egg/manifest.json`.
When a valid manifest exists, the framework skips redundant I/O.

Key design:
- ManifestStore.resolveModule(path, fallback) and globFiles(dir, fallback)
  encapsulate cache-read + collect logic in one place
- All paths stored as relative (forward slashes) for cross-platform portability
- Stat-based fingerprinting (lockfile + config dir) for invalidation
- Generic `extensions: Record<string, unknown>` for plugin-specific data
- `metadataOnly` mode + `loadMetadata` lifecycle hook for manifest generation
- ManifestStore.createCollector() for collection-only mode (no cached data)

Closes #5842

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant